/* -*- mode: c++ -*- */
// Parser grammar for beancount 3.0 input syntax (Bison C++).

//- Options -------------------------------------------------------------------

%require "3.7"

%language "c++"
%define api.namespace {beancount::parser}
%define api.parser.class {Parser}
%define parse.error detailed
%define parse.trace

// %verbose, have to specify the output file.
// %token-table // Has no effect on C++ code generation.

%defines
%locations
%define api.filename.type "const std::string"

// Input state to parser.
// TODO(blais): indent properly, use %param is equivalent?
%parse-param {::beancount::scanner::Scanner& scanner} {::beancount::parser::Builder& builder}

//- Code blocks ---------------------------------------------------------------

// Inserted in header, before anything else
%code requires {

#include "beancount/cparser/parser.pb.h"
#include "beancount/cparser/options.pb.h"
#include "beancount/cparser/ledger.h"
#include "beancount/ccore/data.pb.h"
#include "beancount/ccore/number.pb.h"

#include <cstdint>
#include <cstdio>
#include <fstream>
#include <list>
#include <memory>
#include <algorithm>
#include <string>
#include <optional>

#include "absl/strings/string_view.h"
#include "absl/strings/str_join.h"
#include "absl/time/civil_time.h"
#include "google/protobuf/descriptor.pb.h"
#include "google/protobuf/text_format.h"
#include "decimal.hh"

namespace beancount {
namespace scanner { class Scanner; class ScannerWithFile; }
namespace parser {
class Builder;

// Parse the contents of a file.
std::unique_ptr<Ledger> ParseFile(const std::string_view& filename);

// An intermediate data structure holding the partially parsed tags and links.
struct TagsLinks {
  std::vector<std::string_view> tags;
  std::vector<std::string_view> links;
};

}  // namespace parser
}  // namespace beancount
}  // %code requires

// Inserted in header, at the end.
%code provides {
}  // %code provides

//-----------------------------------------------------------------------------
// Inserted in implementation after including parser header.
%code top {

#include "beancount/cparser/scanner.h"  /* Generated by reflex. */
#include "beancount/ccore/number.h"

#include <iostream>
#include <list>
#include <string>
#include <unordered_map>
#include <utility>

#include "absl/container/flat_hash_map.h"
#include "absl/container/flat_hash_set.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/strings/str_format.h"
#include "absl/strings/string_view.h"
#include "absl/strings/escaping.h"
#include "reflex/input.h"

namespace beancount {

using std::string_view;
using std::cerr;
using std::cout;
using std::endl;
using std::list;
using std::pair;
using std::string;
using std::unique_ptr;
using std::vector;
using absl::StrFormat;
using google::protobuf::FieldDescriptor;
using google::protobuf::TextFormat;

namespace scanner {
// TODO(blais): Move this code to scanner.lxx?

// An extension to the generated scanner that also fetches the filename for the
// location.
class ScannerWithFile : public scanner::Scanner {
public:
  using scanner::Scanner::Scanner;

  void SetLineOffset(const int line_offset) {
    line_offset_ = line_offset;
  }

#if 0
  // TODO(blais): You'll probably remove this, use the @$ Bison construct or
  // default action to set the global location for the rule in the parser.
  // https://www.gnu.org/software/bison/manual/html_node/Location-Default-Action.html

  // Override the location to include the filename.
  beancount::parser::location location() override {
    /* yylloc.begin.line = static_cast<unsigned int>(matcher().lineno()); */
    /* yylloc.begin.column = static_cast<unsigned int>(matcher().columno()); */
    /* yylloc.end.line = static_cast<unsigned int>(matcher().lineno_end()); */
    /* yylloc.end.column = static_cast<unsigned int>(matcher().columno_end()); */
    auto yylloc = scanner::Scanner::location();
    yylloc.begin.filename = &this->filename;
    if (line_offset_ > 0) {
      yylloc.begin.line += line_offset_;
      yylloc.end.line += line_offset_;
    }
    return yylloc;
  }
#endif

  beancount::parser::location get_location() const {
    // Get around insufficiently const generated RE/flex base.
    return const_cast<ScannerWithFile*>(this)->location();
  }

  // TODO(blais): Do we still need this? At least catch the exception.
  virtual void lexer_error(const char* message) override {
    cerr << "Scanner::lexer_error: ";
    if (message) {
      cerr << *message;
    }
    cerr << endl;
    return scanner::Scanner::lexer_error(message);
  }

private:
  int line_offset_ = 0;
};

}  // namespace scanner
namespace parser {

// Convert a date object to an equivalent proto.
// TODO(blais): Move this to ccore.
inline void DateToProto(const absl::CivilDay& date, Date* proto) {
  assert(proto != nullptr);
  proto->set_year(date.year());
  proto->set_month(date.month());
  proto->set_day(date.day());
}

// In-memory build state for graph of nodes. This driver accepts actions from
// the parser and simply builds up the parsed state. Since the input is not
// assumed to be ordered, resolving references occurs as a separate phase.
class Builder {
public:
  Builder(const scanner::ScannerWithFile& scanner);
  virtual ~Builder() {
    for (auto* directive : directives_) delete directive;
    for (auto* error : errors_) delete error;
  }

  // TODO(blais): Review this, not needed.
  void Initialize() {
    //// kAccountRE(StrFormat("(?:%s)(?:%s%s)+",

    // Set Decimal context before processing, update the desired precision for
    // arithmetic operations.
    //
    // Note: Review this, perhaps allow the user to override it (would require
    // post-poning the computation of arithmetic expressions).
    context_ = decimal::context;
    context_.prec(28);
  }

  inline decimal::Context& context() { return context_; }

  const absl::flat_hash_set<string>& active_tags() const { return active_tags_; }
  const absl::flat_hash_map<string, vector<MetaValue*>>& active_meta() const {
    return active_meta_;
  }

  // Add a new option. This gets store into the options proto and some of the
  // values may influence parsing.
  void AddOption(string_view key, string_view value) {
    // Check the options field and get relevant descriptors.
    const auto* descriptor = options_->GetDescriptor();
    const auto* field = descriptor->FindFieldByName(string(key));
    if (!field) {
      LogError(StrFormat("Invalid option: '%s'", key));
      return;
    }

    // Issue a warning if the option is deprecated.
    if (field->options().deprecated()) {
      LogError(StrFormat("Option '%s' is deprecated; ignored.", key));
      return;
    }

    // Set the field on the options proto.
    string value_str(value);
    if (field->type() == FieldDescriptor::TYPE_STRING) {
      value_str = absl::StrCat("\"", absl::CEscape(value_str), "\"");
    }
    if (!TextFormat::ParseFieldValueFromString(value_str, field, options_.get())) {
      LogError(StrFormat("Could not parse and set option '%s' with value '%s'; ignored.",
                         key, value));
      return;
    }

    // TODO(blais): Handle "inferred_tolerance_default" after review, it's for a
    // map. They require special handling. This is a map.
    //
    //      [Opt("inferred_tolerance_default", {}, "CHF:0.01",
    //           converter=options_validate_tolerance_map)]),

    // TODO(blais): Do this.
    // This just requires converted to a Decimal object.
    //       Opt("inferred_tolerance_multiplier", D("0.5"), "1.1",
    //           converter=D)]),
    //   ParserError(meta, "Error for option '{}': {}".format(key, exc),
  }

  void AddInclude(string_view filename) {
    info_->add_include(filename.data(), filename.size());
  }

  void AddPlugin(string_view name, optional<string_view> config) {
    auto* plugin = info_->add_plugin();
    plugin->set_name(name.data(), name.size());
    if (config.has_value()) {
      plugin->set_config(config.value().data(), config.value().size());
    }
  }


  // // Set the location for errors to occur.
  // // This is a stateful setter whose data get used to report errors.
  // void WithLocation(const parser::location& location) {
  //   location_ = location;
  // }

  // TODO(blais): Create an updated driver for Beancount.

  // TODO(blais): Remove this and replace it with post-parsing verification.
  string_view Account(string_view account) {
    accounts_.insert({account.data(), account.size()});
    return account;
  }

  //---------------------------------------------------------------------------
  // Tags & links

  // Update the given repeated field with the list of active tags.
  // This function mutates the `directive` argument.
  // `tags_links` is optional.
  void SetTagsAndLinks(const TagsLinks* tags_links, Directive* message) const {
    if (!active_tags_.empty()) {
      // First copy the active tags.
      auto* output = message->mutable_tags();
      for (const auto& active_tag : active_tags_) {
        *output->Add() = active_tag;
      }
    }

    if (tags_links != nullptr) {
      // Add the new tags.
      if (!tags_links->tags.empty()) {
        // Avoiding duplicates with active metadata.
        for (const auto& tag : tags_links->tags) {
          if (active_tags_.find(tag) == active_tags_.end()) {
            *message->mutable_tags()->Add() = tag;
          }
        }
      }

      // Add the new links.
      if (!tags_links->links.empty()) {
        auto* output = message->mutable_links();
        for (const auto& link : tags_links->links) {
          *output->Add() = link;
        }
      }
    }
  }

  void PushTag(string_view tag) {
    active_tags_.insert({tag.data(), tag.size()});
  }

  void PopTag(string_view tag) {
    auto iter = active_tags_.find({tag.data(), tag.size()});
    if (iter != active_tags_.end()) {
      active_tags_.erase(iter);
    } else {
      LogError(StrFormat("Attempting to pop absent tag: '%s'", tag));
    }
  }

  //---------------------------------------------------------------------------
  // Active metadata

  // Steal the reference to `value`.
  void PushMeta(string_view key, MetaValue* value) {
    auto& value_list = active_meta_[key];
    value_list.push_back(value);
  }

  void PopMeta(string_view key) {
    auto iter = active_meta_.find(key);
    if (iter == active_meta_.end()) {
      LogError(StrFormat("Attempting to pop absent metadata key: '%s'", key));
    } else {
      auto& value_list = iter->second;
      assert(value_list.size() > 0);
      auto* value = value_list.back();
      value_list.pop_back();
      if (value_list.empty()) {
        active_meta_.erase(iter);
      }
      delete value;
    }
  }

  // Insert the active metadata into the given argument. If there is no metadata
  // to be added, the directive is unmodified.
  inline void AddActiveMetadata(Meta* meta,
                                Directive* dir) const {
    // Append active metadata into the output.
    if (!active_meta_.empty()) {
      Meta* dirmeta = dir->mutable_meta();
      for (auto [key, value_list] : active_meta_) {
        auto* kv = dirmeta->mutable_kv()->Add();
        kv->set_key(key);
        kv->mutable_value()->CopyFrom(*value_list.back());
      }
    }

    // Merge given metadata on top of active metadttaa.
    if (meta != nullptr) {
      Meta* dirmeta = dir->mutable_meta();
      dirmeta->MergeFrom(*meta);
    }

    // TODO(blais): Check for the presence of duplicates in the loop above and
    // log and error if found.
    //
    //  value = explicit_meta.setdefault(posting_or_kv.key,
    //                                   posting_or_kv.value)
    //  if value is not posting_or_kv.value:
    //      self.errors.append(ParserError(
    //          meta, "Duplicate metadata field on entry: {}".format(
    //              posting_or_kv), None))
    //
  }

  //---------------------------------------------------------------------------
  // Numbers & precision

  // Convert a decimal number to a proto.
  void DecimalProto(const decimal::Decimal& dec, Number* proto) {
    // Note you could configure conversion options here.
    DecimalToProto(dec, true, proto);
  }

  void UpdatePrecisionStats(const decimal::Decimal& dec, string_view currency) {
    // TODO(blais): Update display context stats. See grammar.Builder.dcupdate().
  }





  // Create a new directive. Return value ownership is given. If `meta` is
  // non-null, it will be reset and ownership will be taken (and it will be
  // deleted).
  inline Directive* MakeDirective(const absl::CivilDay& date,
                                  Meta** meta,
                                  TagsLinks** tags_links) const {
    auto* dir = new Directive();

    // Set the date.
    DateToProto(date, dir->mutable_date());

    // Copy the metadata, if provided, including the stack's metadata.
    Meta* ourmeta = nullptr;
    if (meta != nullptr) {
      ourmeta = *meta;
      meta = nullptr;
    }
    AddActiveMetadata(ourmeta, dir);
    delete ourmeta;

    // Update tags, including adding active tags, and links.
    SetTagsAndLinks(*tags_links, dir);
    if (tags_links != nullptr && *tags_links != nullptr) {
      delete *tags_links;
      tags_links = nullptr;
    }

    return dir;
  }

  // Note: We steal ownership.
  void AppendDirective(Directive* directive) {
    directives_.push_back(directive);
  }

  // Common posting preparation actions.
  void PreparePosting(Posting* posting,
                      const char flag,
                      string_view account,
                      bool is_total,
                      const location& loc) {
    assert(posting != nullptr);

    // Store flag and account name.
    if (flag != '\0') {
      posting->set_flag(&flag, 1);
    }
    posting->set_account(account.data(), account.size());

    // Check conditions renamed to price annotations.
    if (posting->has_price()) {
      const auto& price = posting->price();

      // If the price is specified for the entire amount, compute the effective
      // price here and forget about that detail of the input syntax.
      if (is_total) {
        if (!posting->has_units() || !posting->units().has_number()){
          // units.number is MISSING.
          // Note: we could potentially do a better job and attempt to f
          // this up after interpolation, but this syntax is pretty rare
          // anyway.
          LogError(StrFormat("Total price on a posting without units: %s.",
                             price.DebugString()));
          posting->clear_price();
        } else if (price.has_number()) {
          decimal::Decimal dunits = ProtoToDecimal(posting->units().number());
          decimal::Decimal dprice;
          if (dunits.iszero()) {
            dprice = dunits;
          } else {
            dprice = ProtoToDecimal(price.number()).div(dunits.abs(), context());
          }
          DecimalProto(dprice, posting->mutable_price()->mutable_number());
        }
      }

      // Note: Allow zero prices because we need them for round-trips for
      // conversion entries.
      //
      // if price is not None and price.number == ZERO:
      //     self.errors.append(
      //         ParserError(meta, "Price is zero: {}".format(price), None))

      // If both cost and price are specified, the currencies must match, or
      // that is an error.
      if (posting->has_cost_spec() &&
          posting->cost_spec().has_currency() &&
          price.has_currency() &&
          posting->cost_spec().currency() != price.currency()) {
        LogError(StrFormat("Cost and price currencies must match: %s != %s",
                           posting->cost_spec().currency(), price.currency()));
      }
    }
  }

  // Insert an error in the error log. These will be returned along with the
  // ledger as output of the parsing phase.
  void LogError(string_view message) {
    // TODO(blais):
    /// scanner_.location()
    std::cerr << scanner_.get_location() << ": " << message << std::endl;
  }
  // TODO(blais): Can we turn this error logging into a stream instead?

  // Check that all the accounts are matching the prefix names provided in the
  // options.
  void ValidateAccountNames() {
  // TODO(blais):

    // Validate the account root names are valid in the options.

    //     # Refresh the list of valid account regexps as we go along.
    //     if key.startswith('name_'):
    //         # Update the set of valid account types.
    //         self.account_regexp = valid_account_regexp(self.options)

    // if not self.account_regexp.match(account):
    //     meta = new_metadata(filename, lineno)
    //     self.errors.append(
    //         ParserError(meta, "Invalid account name: {}".format(account), None))
  }

  void Finalize() {
    void ValidateAccountNames();

    // If the user left some tags unbalanced, issue an error.
    for (const auto& tag : active_tags_) {
      LogError(StrFormat("Unbalanced pushed tag: '%s'", tag));
    }

    // If the user left some metadata unpopped, issue an error.
    for (const auto& [key, value_list] : active_meta_) {
      LogError(StrFormat(
                 "Unbalanced metadata key '%s' has leftover metadata", key));
    }
  }

  // TODO(blais): Process this with code in the plugins loading loop.
    //     elif key == 'insert_pythonpath':
    //         # Insert the PYTHONPATH to this file when and only if you
    //         # encounter this option.
    //         sys.path.insert(0, path.dirname(filename))

  // Build an instance of a new ledger. This is how one stamps and extracts all
  // the results from the parser. The parser isn't reset.
  std::unique_ptr<Ledger> MakeLedger() {
    auto ledger = std::make_unique<Ledger>();
    ledger->directives = std::move(directives_);
    ledger->errors = std::move(errors_);
    ledger->options = std::move(options_);
    ledger->info = std::move(info_);
    assert(ledger->options);
    assert(ledger->info);
    return ledger;
  }

private:
  // The accumulated state, same as Ledger.
  std::list<Directive*> directives_;
  std::vector<Error*> errors_;
  std::shared_ptr<options::Options> options_;
  std::shared_ptr<options::ProcessingInfo> info_;

  // Decimal context.
  decimal::Context context_;

  // Scanner (in order to get the last location).
  const scanner::ScannerWithFile& scanner_;

  // A set of all unique account names seen in the file.
  absl::flat_hash_set<string> accounts_;

  // A set of active tags.
  absl::flat_hash_set<string> active_tags_;

  // A dict of the current active metadata fields; each of the field values is a
  // stack and the current value is at the top (last value).
  absl::flat_hash_map<string, vector<MetaValue*>> active_meta_;
};

}  // namespace parser
}  // namespace beancount
}  // %code top

// Inserted in implementation before including parser header
// (Within bison's parse() we should invoke lexer.lex(), not the global yylex().)
%code {
#undef yylex
#define yylex scanner.lex
}

//- Tokens --------------------------------------------------------------------
// Note: Token type names appear in the parser header; qualify fully.

/* Collection of value types. */
%define api.value.type variant // See: "C++ Variants"
%define api.token.constructor
%define api.token.prefix {TOK_}

/* Special error token, redefined to carry an error message. */
%token <std::string_view> error

/* Types for terminal symbols */
%token INDENT     /* Initial indent IF at the beginning of a line */
%token DEDENT     /* Initial indent IF at the beginning of a line */
%token EOL        /* End-of-line */
%token PIPE       /* | */
%token ATAT       /* @@ */
%token AT         /* @ */
%token LCURLCURL  /* {{ */
%token RCURLCURL  /* }} */
%token LCURL      /* { */
%token RCURL      /* } */
%token COMMA      /* , */
%token TILDE      /* ~ */
%token HASH       /* # */
%token ASTERISK   /* * */
%token SLASH      /* / */
%token COLON      /* : */
%token PLUS       /* + */
%token MINUS      /* - */
%token LPAREN     /* ( */
%token RPAREN     /* ) */
%token <char> FLAG    /* Valid characters for flags */
%token TXN                 /* 'txn' keyword */
%token BALANCE             /* 'balance' keyword */
%token OPEN                /* 'open' keyword */
%token CLOSE               /* 'close' keyword */
%token COMMODITY           /* 'commodity' keyword */
%token PAD                 /* 'pad' keyword */
%token EVENT               /* 'event' keyword */
%token PRICE               /* 'price' keyword */
%token NOTE                /* 'note' keyword */
%token DOCUMENT            /* 'document' keyword */
%token QUERY               /* 'query' keyword */
%token CUSTOM              /* 'custom' keyword */
%token PUSHTAG             /* 'pushtag' keyword */
%token POPTAG              /* 'poptag' keyword */
%token PUSHMETA            /* 'pushmeta' keyword */
%token POPMETA             /* 'popmeta' keyword */
%token OPTION              /* 'option' keyword */
%token INCLUDE             /* 'include' keyword */
%token PLUGIN              /* 'plugin' keyword */
%token NONE        /* A None value (parsed as NULL) */
%token <bool> BOOL        /* A boolean, true or false */
%token <absl::CivilDay> DATE        /* A date object */
%token <decimal::Decimal> NUMBER      /* A decimal number */
%token <std::string_view> ACCOUNT     /* The name of an account */
%token <std::string_view> CURRENCY    /* A currency specification */
%token <std::string_view> STRING      /* A quoted string, with any characters inside */
%token <std::string_view> TAG         /* A tag that can be associated with a transaction */
%token <std::string_view> LINK        /* A link that can be associated with a transaction */
%token <std::string_view> KEY         /* A key in a key-value pair */

/* Types for non-terminal symbols. */
%type <char> txn
%type <char> optflag
%type <std::string_view> account

%type <beancount::Directive*> transaction_line

%type <std::vector<beancount::Posting*>> posting_list
%type <beancount::Posting*> posting_and_metadata
%type <beancount::Posting*> posting

%type <beancount::Meta*> indented_metadata
%type <beancount::Meta*> metadata
%type <beancount::Meta::KV*> metadata_line
%type <beancount::MetaValue*> meta_value
%type <std::vector<beancount::MetaValue*>> meta_value_list

/* TODO(blais): Add constness aggressively. The produced tokens can be const. */
%type <std::vector<std::string>> currency_list
%type <std::pair<const beancount::Amount*, std::optional<decimal::Decimal>>> amount_tolerance
%type <beancount::Amount*> amount
%type <beancount::Amount*> partial_amount
%type <std::optional<decimal::Decimal>> maybe_number
%type <std::optional<std::string_view>> maybe_currency
%type <beancount::Amount*> price_annotation
%type <beancount::inter::CostSpec*> compound_amount
%type <beancount::inter::CostSpec*> cost_comp
%type <beancount::inter::CostSpec*> cost_comp_list
%type <beancount::inter::CostSpec*> cost_spec

%type <beancount::Directive*> directive
%type <beancount::Directive*> transaction
%type <beancount::Directive*> balance
%type <beancount::Directive*> open
%type <beancount::Directive*> close
%type <beancount::Directive*> pad
%type <beancount::Directive*> document
%type <beancount::Directive*> note
%type <beancount::Directive*> event
%type <beancount::Directive*> price
%type <beancount::Directive*> commodity
%type <beancount::Directive*> query
%type <beancount::Directive*> custom

%type <std::pair<std::string, std::string>> payee_narration
%type <beancount::parser::TagsLinks*> tags_links
%type <std::string_view> filename
%type <options::Booking> booking
%type <decimal::Decimal> number_expr

%type option
%type include
%type plugin

%type pushtag
%type poptag
%type pushmeta
%type popmeta

%type file
%type declarations

/* Operator precedence.
 * This is pulled straight out of the textbook example:
 * https://www.gnu.org/software/bison/manual/html_node/Infix-Calc.html#Infix-Calc
 */
%left MINUS PLUS
%left ASTERISK SLASH
%precedence NEGATIVE /* negation--unary minus */

/* Start symbol. */
%start file

/* We have some number of expected shift/reduce conflicts at 'eol'. */
/* TODO(blais): Have another go at taking to zero. */
%expect 19



/*- Grammar rules -----------------------------------------------------------*/
%%

/* Start token for parsing an entire file of the DSL. */
file:
  declarations YYEOF

/* End-of-line marker. In Beancount whitespace matters, so we don't swallow these and
   match them explicitly. */
eol: EOL | YYEOF

/* A rule to reduce pragmas and directives. We also handle errors at this
   level. */
declarations:
  declarations EOL
  /* Accumulate nothing for anything that's not a directive. The side-effects of
     all those pragma are stored in the builder object. */
  | declarations pragma
  /* A regular directive. Accmulate it in the builder. */
  | declarations directive
    {
      // Note: the reference is given to the builder.
      builder.AppendDirective($directive);

      // Set the location.
      auto* loc = $directive->mutable_location();
      loc->set_filename(scanner.filename);
      loc->set_lineno(@2.begin.line);
      // TODO(blais): Bring this in after 1:1 parity.
      // loc->set_lineno_end(@2.end.line);
    }
  /* An error raised at the parser level. */
  | declarations error
    {
      // TODO(blais): What about the 'error' token encountered elsewhere? Is this
      // guaranteed to bubble up here?
      //
      // TODO(blais): Great place to generically log and error here too?

      // Ignore the error and continue reducing ({3d95e55b654e}). Note that with
      // the matching rule above, "error" will successfully reduce on each line
      // that cannot reduce. Non-erroneous postings after an error occurs will
      // reduce but not be included because a transaction's list of postings
      // does not include an "error" rule.
      //
      // Note: Adding EOL after the "error" rule above works to reduce the
      // number of calls to this rule resulting from the appearance of an error
      // but makes the parser errors skip the next valid directive, so we just
      // have to make sure repeated runs of this rule's handling code are
      // idempotent.
    }
  | %empty

/*- Pragmas -----------------------------------------------------------------*/

/* pragmas: Anything but a directive. Those constructs apply side-effects to the
   builder object. */
pragma:
  pushtag
  | poptag
  | pushmeta
  | popmeta
  | option
  | include
  | plugin

/* A pragma that pushes a new tag applied to transactions. */
pushtag:
  PUSHTAG TAG eol
    {
      builder.PushTag($2);
    }

/* Remove a specific tag from the active list. */
poptag:
  POPTAG TAG eol
    {
      builder.PopTag($2);
    }

/* A pragma that pushes a new key-value pair applied to transactions. */
pushmeta:
  PUSHMETA KEY COLON meta_value eol
    {
      builder.PushMeta($2, $4);
    }

/* Remove a specific key-value pairfrom the active list. */
popmeta:
  POPMETA KEY COLON eol
    {
      builder.PopMeta($2);
    }

/* An option pragma. */
option:
  OPTION STRING[key] STRING[value] eol
    {
      builder.AddOption($key, $value);
    }

/* An include pragma. */
include:
  INCLUDE STRING eol
    {
      builder.AddInclude($2);
    }

/* A plugin pragma, along with its optional configuration. */
plugin:
  PLUGIN STRING eol
    {
      builder.AddPlugin($2, {});
    }
  | PLUGIN STRING STRING eol
    {
      builder.AddPlugin($2, $3);
    }

/*- Directives --------------------------------------------------------------*/

/* A rule to reduce any of the Beancount directives we offer. */
directive:
  transaction
  | price
  | balance
  | open
  | close
  | commodity
  | pad
  | document
  | note
  | event
  | query
  | custom

/* A Transaction directive, including postings. */
transaction:
  /* A transaction with no indented body (this is valid). */
  transaction_line
    {
      $$ = $1;
    }
  /* A transaction with an indented body. */
  | transaction_line INDENT metadata posting_list DEDENT
    {
      $$ = $1;

      // Insert the local metadata on top of that from the directive.
      // {99ef4ca51cd5}
      if ($metadata != nullptr) {
        $$->mutable_meta()->MergeFrom(*$metadata);
        delete $metadata;
      }

      // Attach postings to the directive.
      auto* txn = $$->mutable_transaction();
      for (const auto* posting : $posting_list) {
        txn->mutable_postings()->Add()->CopyFrom(*posting);
      }
    }

/* Matches just the first line of a transaction, without its metadata. */
transaction_line:
  DATE txn[flag] payee_narration[pn] tags_links eol
    {
      // Create directive and update head, without custom metadata. That is
      // added at {99ef4ca51cd5}.
      $$ = builder.MakeDirective($1, nullptr, &$tags_links);

      // Create Transaction and update flag.
      auto* txn = $$->mutable_transaction();
      if ($flag != '\0') {
        txn->set_flag(&$flag, 1);
      }

      // Update payee and narration fields.
      if (!$pn.first.empty()) {
        txn->set_payee($pn.first.data(), $pn.first.size());
      }
      if (!$pn.second.empty()) {
        txn->set_narration($pn.second.data(), $pn.second.size());
      }
    }

/* A Price directive. */
price:
  DATE PRICE CURRENCY amount tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* price = $$->mutable_price();
      price->set_currency($3.data(), $3.size());
      price->mutable_amount()->CopyFrom(*$amount);
    }

/* A Balance directive. */
balance:
  DATE BALANCE account amount_tolerance[atol] tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* balance = $$->mutable_balance();
      balance->set_account($account.data(), $account.size());

      balance->mutable_amount()->CopyFrom(*$atol.first);
      delete $atol.first;

      if ($atol.second.has_value()) {
        builder.DecimalProto($atol.second.value(), balance->mutable_tolerance());
      }
      // Note: We leave `diff_amount` unset.
    }

/* An Open directive. */
open:
  DATE OPEN account currency_list booking tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* open = $$->mutable_open();
      open->set_account($account.data(), $account.size());

      for (const auto& currency : $currency_list) {
        open->add_currencies(currency);
      }

      if ($booking != options::Booking::UNKNOWN) {
        open->set_booking($booking);
      }
    }

/* A Close directive. */
close:
  DATE CLOSE account tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* open = $$->mutable_close();
      open->set_account($account.data(), $account.size());
    }

/* A Commodity directive. */
commodity:
  DATE COMMODITY CURRENCY tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* commodity = $$->mutable_commodity();
      commodity->set_currency($CURRENCY.data(), $CURRENCY.size());
    }

/* A Pad directive. */
pad:
  DATE PAD account account[source] tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* pad = $$->mutable_pad();
      pad->set_account($3.data(), $3.size());
      pad->set_source_account($source.data(), $source.size());
    }

/* A Document directive. */
document:
  DATE DOCUMENT account filename tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* document = $$->mutable_document();
      document->set_account($account.data(), $account.size());
      document->set_filename($filename.data(), $filename.size());
    }

/* A Note directive. */
note:
  DATE NOTE account STRING[comment] tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* note = $$->mutable_note();
      note->set_account($3.data(), $3.size());
      note->set_comment($comment.data(), $comment.size());
    }

/* An Event directive. */
event:
  DATE EVENT STRING[type] STRING[description] tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* event = $$->mutable_event();
      event->set_type($type.data(), $type.size());
      event->set_description($description.data(), $description.size());
    }

/* A Query directive. */
query:
  DATE QUERY STRING[name] STRING[qstr] tags_links eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, &$tags_links);
      auto* query = $$->mutable_query();
      query->set_name($name.data(), $name.size());
      query->set_query_string($qstr.data(), $qstr.size());
    }

/* A Custom directive. Note that this directive does not support the dedicated
   tags and links because those can be part of metadata. It would be ambiguous
   which they are. */
custom:
  DATE CUSTOM STRING[type] meta_value_list eol indented_metadata
    {
      $$ = builder.MakeDirective($1, &$indented_metadata, nullptr);
      auto* custom = $$->mutable_custom();
      custom->set_type($type.data(), $type.size());

      // Copy custom values here.
      for (const auto* meta_value : $meta_value_list) {
        // TODO(blais): Will AddAllocated() work here?
        custom->mutable_values()->Add()->CopyFrom(*meta_value);
        delete meta_value;
      }
      $meta_value_list.clear();
    }

/*- Metadata ----------------------------------------------------------------*/

/* A list of metadata (optionally empty), including its indentation. */
indented_metadata:
  %empty
    {
      $$ = nullptr;
    }
  | INDENT metadata DEDENT
    {
      $$ = $2;
    }

/* A list of metadata (optionally empty). */
metadata:
  %empty
    {
      $$ = nullptr;
    }
  | metadata metadata_line
    {
      if ($1 == nullptr) {
        $$ = new beancount::Meta();
      } else {
        $$ = $1;
      }
      auto* kv = $$->add_kv();
      kv->CopyFrom(*$2);
      delete $2;
    }

/* A single line of a metadata declaration. */
metadata_line:
  /* Regular key-value pairs. */
  KEY COLON meta_value eol
    {
      $$ = new beancount::Meta::KV();
      $$->set_key($1.data(), $1.size());
      $$->mutable_value()->CopyFrom(*$3);
      delete $3;
    }
  /* A tag can be used by itself as metadata. */
  | TAG eol
    {
      $$ = new beancount::Meta::KV();
      $$->mutable_value()->set_tag($1.data(), $1.size());
    }
  /* A link can be used by itself as metadata. */
  | LINK eol
    {
      $$ = new beancount::Meta::KV();
      $$->mutable_value()->set_link($1.data(), $1.size());
    }

/* A general variant that can contain basic types, used as the value in
   key-value pairs of metadata and in Custom directives. */
meta_value:
  STRING
    {
      $$ = new beancount::MetaValue();
      $$->set_text($1.data(), $1.size());
    }
  | CURRENCY
    {
      $$ = new beancount::MetaValue();
      $$->set_currency($1.data(), $1.size());
    }
  | account
    {
      $$ = new beancount::MetaValue();
      $$->set_account($1.data(), $1.size());
    }
  | TAG
    {
      $$ = new beancount::MetaValue();
      $$->set_tag($1.data(), $1.size());
    }
  | LINK
    {
      $$ = new beancount::MetaValue();
      $$->set_link($1.data(), $1.size());
    }
  | DATE
    {
      $$ = new beancount::MetaValue();
      DateToProto($1, $$->mutable_date());
    }
  | BOOL
    {
      $$ = new beancount::MetaValue();
      $$->set_boolean($1);
    }
  | NONE
    {
      $$ = new beancount::MetaValue();
      /* NULL is a proto not set */
    }
  | number_expr
    {
      $$ = new beancount::MetaValue();
      builder.DecimalProto($1, $$->mutable_number());
    }
  /* TODO(blais): Bring this back once 'amount' has been ported. */
  // | amount
  //   {
  //     $$ = new beancount::MetaValue();
  //     builder.DecimalProto($1, $$->mutable_number());
  //   }
  | %empty
    {
      $$ = new beancount::MetaValue();
    }

/* A list of variant values. */
meta_value_list:
  %empty
    {
      $$ = {};
    }
  | meta_value_list meta_value
    {
      $$ = $1;
      $$.push_back($2);
    }

/* A container for tags and links, which show up at the end of a transaction's
   first line or in some of the directives. */
tags_links:
  %empty
    {
      $$ = nullptr;
    }
  | tags_links TAG
    {
      $$ = ($1 != nullptr) ? ($1) : new TagsLinks{};
      $$->tags.emplace_back($2.data(), $2.size());
    }
  | tags_links LINK
    {
      $$ = ($1 != nullptr) ? ($1) : new TagsLinks{};
      $$->links.emplace_back($2.data(), $2.size());
    }

/*- String matchers ---------------------------------------------------------*/

/* Payee and narration strings. */
payee_narration:
  /* Variant when both strings are present. */
  STRING STRING
    {
      $$ = make_pair($1, $2);
    }
  /* Variant when only one string is present; interpret as narration only. */
  | STRING
    {
      $$ = make_pair("", $1);
    }

/* An  account name. */
account:
  ACCOUNT
    {
      $$ = builder.Account($1);
    }

/* A filename. Consider specializing this. */
filename:
  STRING

/* A comma-separated list of currencies. */
currency_list:
  %empty
    {
      $$ = {};
    }
  | CURRENCY
    {
      $$.push_back({$1.data(), $1.size()});
    }
  | currency_list COMMA CURRENCY
    {
      $$ = $1;
      $$.push_back({$3.data(), $3.size()});
    }

/*- Arithmetic expressions --------------------------------------------------*/

/* Arithmetic expression, reduced to a number. The reduction is carried out in
   the parser. */
number_expr:
  NUMBER
  | number_expr PLUS number_expr
    {
      $$ = $1.add($3, builder.context());
    }
  | number_expr MINUS number_expr
    {
      $$ = $1.sub($3, builder.context());
    }
  | number_expr ASTERISK number_expr
    {
      $$ = $1.mul($3, builder.context());
    }
  | number_expr SLASH number_expr
    {
      $$ = $1.div($3, builder.context());
    }
  | MINUS number_expr %prec NEGATIVE
    {
      $$ = $2.minus(builder.context());
    }
  | PLUS number_expr %prec NEGATIVE
    {
      // Note: Not entirely sure we shouldn't just copy.
      $$ = $2.plus(builder.context());
    }
  | LPAREN number_expr RPAREN
    {
      $$ = $2;
    }

/*- Single-character flags & other constants --------------------------------*/

/* The flag in a transaction declaration: it can be either 'txn' or one of the
   special character flags. */
txn:
  TXN
    {
      $$ = '*';
    }
  | FLAG
    {
        $$ = $1;
    }
  | ASTERISK
    {
        $$ = '*';
    }
  | HASH
    {
        $$ = '#';
    }

/* An optional flag marking a posting. */
optflag:
  %empty
    {
      $$ = '\0';
    }
  | ASTERISK
    {
      $$ = '*';
    }
  | HASH
    {
      $$ = '#';
    }
  | FLAG

/* The declaration of a booking method as a string.
   TODO(blais): Turn this to constants, strings was for v2. */
booking:
  STRING
    {
      string s1($1.data(), $1.size());
      options::Booking method;
      if (!Booking_Parse(s1, &method)) {
        builder.LogError(StrFormat("Invalid value for booking method: '%s'", s1));
        // TODO(blais): For v2 behavior, set this to the global default.
        method = options::Booking::UNKNOWN;
      }
      $$ = method;
    }
  | %empty
    {
      $$ = options::Booking::UNKNOWN;
    }

/*- Postings ----------------------------------------------------------------*/

/* A list of postings, possibly empty. */
posting_list:
  %empty
    {
      $$ = {};
    }
  /* Matches an empty or comment indented line. It's convenient to be able to
   * comment indented postings. */
  | posting_list eol
  /* Matches an actual posting. */
  | posting_list posting_and_metadata
    {
      $1.push_back($2);
      $$ = $1;
    }

/* A single posting and its associated metadata. */
posting_and_metadata:
  posting indented_metadata
    {
      if ($2 != nullptr) {
        $1->mutable_meta()->CopyFrom(*$2);
        delete $2;
      }
      $$ = $posting;

      // Set the location.
      auto* loc = $posting->mutable_location();
      loc->set_filename(scanner.filename);
      loc->set_lineno(@1.begin.line);
      loc->set_lineno_end(@2.end.line);
    }

/* A posting line, without any of its metadata. */
posting:
  /* Variation with no price. */
  optflag account partial_amount cost_spec eol
    {
      $$ = new Posting();
      $$->mutable_units()->CopyFrom(*$partial_amount);
      if ($cost_spec != nullptr) {
        $$->mutable_cost_spec()->CopyFrom(*$cost_spec);
      }
      builder.PreparePosting($$, $optflag, $account, false, scanner.location());
    }
  /* Variation with price. */
  | optflag account partial_amount cost_spec AT price_annotation eol
    {
      $$ = new Posting();
      $$->mutable_units()->CopyFrom(*$partial_amount);
      if ($cost_spec != nullptr) {
        $$->mutable_cost_spec()->CopyFrom(*$cost_spec);
      }
      $$->mutable_price()->CopyFrom(*$price_annotation);
      builder.PreparePosting($$, $optflag, $account, false, scanner.location());
    }
  /* Variation with complete price. */
  | optflag account partial_amount cost_spec ATAT price_annotation eol
    {
      // TODO(blais): Merge this rule with the one above, by inspecting the
      // value of AT and ATAT.
      $$ = new Posting();
      $$->mutable_units()->CopyFrom(*$partial_amount);
      if ($cost_spec != nullptr) {
        $$->mutable_cost_spec()->CopyFrom(*$cost_spec);
      }
      $$->mutable_price()->CopyFrom(*$price_annotation);
      builder.PreparePosting($$, $optflag, $account, true, scanner.location());
    }
  /* Variation with just an account name. */
  | optflag account eol
    {
      $$ = new Posting();
      builder.PreparePosting($$, $optflag, $account, false, scanner.location());
    }

/* A price annotation on a posting. */
price_annotation:
  partial_amount
    {
      $price_annotation = $partial_amount;

      // Prices may not be negative. Check and issue an error if found; fix up
      // the price to its absolute value and continue.
      if ($price_annotation->has_number()) {
        decimal::Decimal dec = ProtoToDecimal($price_annotation->number());
        if (dec.sign() == -1) {
          builder.LogError("Negative prices are not allowed "
                           "(see http://furius.ca/beancount/doc/bug-negative-prices "
                           "for workaround)");
          // Invert and continue.
          builder.DecimalProto(-dec, $price_annotation->mutable_number());
        }
      }
    }

/*- Amounts & Costs ---------------------------------------------------------*/

/* An Amount, which is a decimal number and a currency. */
amount:
  number_expr CURRENCY
    {
      auto* amount = new Amount();
      builder.DecimalProto($1, amount->mutable_number());
      amount->set_currency($2.data(), $2.size());
      $$ = amount;
      builder.UpdatePrecisionStats($1, $2);
    }

/* An amount, with an optional associated tolerance. */
amount_tolerance:
  amount
    {
      $$ = make_pair($1, std::optional<decimal::Decimal>{});
    }
  | number_expr TILDE number_expr[tolerance] CURRENCY
    {
      auto* amount = new Amount();
      builder.DecimalProto($1, amount->mutable_number());
      amount->set_currency($4.data(), $4.size());
      $$ = make_pair(amount, $tolerance);
      builder.UpdatePrecisionStats($1, $4);
      builder.UpdatePrecisionStats($tolerance, $4);
    }

/* An optional number. */
maybe_number:
  number_expr
    {
      $$ = {$1};
    }
  | %empty
    {
      $$ = {};
    }

/* An optional currency. */
maybe_currency:
  CURRENCY
    {
      $$ = {$1};
    }
  | %empty
    {
      $$ = {};
    }

/* An amount with possibly missing values for number and/or currency. This is
   used in postings to allow users to let Beancount interpolate the missing
   values. */
partial_amount:
  maybe_number maybe_currency
    {
      $$ = new Amount();
      if ($1.has_value()) {
        builder.DecimalProto($1.value(), $$->mutable_number());
      }
      if ($2.has_value()) {
        $$->set_currency($2.value().data(), $2.value().size());
      }
    }

/* A per-unit/total amount specification, to be used in cost specification. */
compound_amount:
  maybe_number CURRENCY
    {
      $$ = new inter::CostSpec();
      if ($1.has_value()) {
        builder.DecimalProto($1.value(), $$->mutable_number_per());
        builder.UpdatePrecisionStats($1.value(), $2);
      }
      $$->set_currency({$2.data(), $2.size()});
    }
  | number_expr maybe_currency
    {
      $$ = new inter::CostSpec();
      builder.DecimalProto($1, $$->mutable_number_per());
      if ($2.has_value()) {
        $$->set_currency({$2.value().data(), $2.value().size()});
        builder.UpdatePrecisionStats($1, $2.value());
      }
    }
  | maybe_number HASH maybe_number CURRENCY
    {
      $$ = new inter::CostSpec();
      if ($1.has_value()) {
        builder.DecimalProto($1.value(), $$->mutable_number_per());
        builder.UpdatePrecisionStats($1.value(), $4);
      }
      if ($3.has_value()) {
        builder.DecimalProto($3.value(), $$->mutable_number_total());
        builder.UpdatePrecisionStats($3.value(), $4);
      }
      $$->set_currency({$4.data(), $4.size()});
    }

/* A cost specification. */
cost_spec:
  LCURL cost_comp_list RCURL
    {
      $$ = $2;
    }
  | LCURLCURL cost_comp_list RCURLCURL
    {
      if ($2 != nullptr) {
        // Note: This is using the total syntax; it is an error to set
        // `number_total` to a value in the input when using `{{...}}`.
        if ($2->has_number_total()) {
          builder.LogError(
            StrFormat("Per-unit cost may not be specified using total cost "
                      "syntax: '%s'; ignoring per-unit cost", $2->DebugString()));
        } else {
          $2->mutable_number_total()->CopyFrom($2->number_per());
        }
        $2->clear_number_per();
      }
      $$ = $2;
    }
  | %empty
    {
      $$ = nullptr;
    }

/* A comma-separated list of components in a {...} cost specification. */
cost_comp_list:
  %empty
    {
      $$ = nullptr;
    }
  | cost_comp
  | cost_comp_list COMMA cost_comp
    {
      $$ = ($1 != nullptr) ? ($1) : new inter::CostSpec();
      // TODO(blais): Check for collisions here.
      $$->MergeFrom(*$3);
      delete $3;
    }

/* One component of a cost specification. */
cost_comp:
  compound_amount
  | DATE
    {
      $$ = new inter::CostSpec();
      DateToProto($1, $$->mutable_date());
    }
  | STRING
    {
      $$ = new inter::CostSpec();
      $$->set_label($1.data(), $1.size());
    }
  | ASTERISK
    {
      $$ = new inter::CostSpec();
      $$->set_merge_cost(true);
    }

%%
//- User code ----------------------------------------------------------------

// TODO(blais): Move this to error() code.
#if 0
/* Error-handling function. {ca6aab8b9748} */
void yyerror(const location& loc, yyscan_t scanner, /* PyObject* builder, */ char const* message)
#endif

// TODO(blais): Review this completely.
void beancount::parser::Parser::error(const location& loc, const string& msg)
{
  // TODO(blais): Change this code to log an error and bubble that up to the
  // output. We should never write to stderr.
  using std::cerr;
  using std::endl;
  cerr << loc << ": " << msg << endl;
  if (loc.begin.line == loc.end.line && loc.begin.line == scanner.lineno()) {
    cerr << scanner.matcher().line() << endl;
    for (size_t i = 0; i < loc.begin.column; ++i)
      cerr << " ";
    for (size_t i = loc.begin.column; i <= loc.end.column; ++i)
      cerr << "~";
    cerr << endl;
  } else {
    FILE* file = scanner.in().file(); // The current file being scanned.
    if (file != NULL) {
      beancount::scanner::Scanner::Matcher* m = scanner.new_matcher(file);
      scanner.push_matcher(m);
      off_t pos = ftell(file);
      fseek(file, 0, SEEK_SET);
      for (size_t i = 1; i < loc.begin.line; ++i) {
        m->skip('\n'); // Skip to the next line.
      }
      for (size_t i = loc.begin.line; i <= loc.end.line; ++i) {
        cerr << m->line() << endl;
        m->skip('\n'); // Next line.
      }
      fseek(file, pos, SEEK_SET); // Restore position in the file to continue scanning.
      scanner.pop_matcher(); // Restore matcher.
    }
  }

  // If token is unknown (no match).
  if (scanner.size() == 0) {
    scanner.matcher().winput(); // Skip character.
  }
}

namespace beancount {
namespace parser {

Builder::Builder(const scanner::ScannerWithFile& scanner) :
  scanner_(scanner)
{
  info_.reset(new options::ProcessingInfo());
  options_.reset(new options::Options());
  Initialize();
}

// TODO(blais): Implement builder methods here (maybe).

}  //  namespace parser

namespace parser {

// TODO(blais): Return the parsed state.
std::unique_ptr<Ledger> Parse(
  const reflex::Input& input,
  const std::optional<string_view>& filename = {},
  int line_offset = 0) {
  // Create a scanner and builder.
  scanner::ScannerWithFile scanner(input, cout);
  if (filename.has_value()) {
    scanner.filename = filename.value();
  }
  scanner.SetLineOffset(line_offset);
  parser::Builder builder(scanner);

  // Run the parser.
  beancount::parser::Parser parser(scanner, builder);
  parser.set_debug_stream(std::cerr);
  if (getenv("DEBUG")) {
    parser.set_debug_level(1);
  }
  // scanner.set_debug(1);
  try {
    int result = parser.parse();
    (void)result; // TODO(blais): Handle the result and corresponding error.

  } catch (const std::runtime_error& exc) {
    cerr << "exc " << exc.what() << endl;
  }
  return builder.MakeLedger();
}

std::unique_ptr<Ledger> ParseFile(const string_view& filename) {
  std::ifstream in = std::ifstream(string(filename), std::ios::in);
  reflex::Input input(in);
  string filename_str(filename);
  return parser::Parse(input, filename_str, 0);
}

// TODO(blais): Implement this.
/// std::unique_ptr<proto::Database> ParseString(const string_view& input_string,
///                                              const string_view& filename,
///                                              int line_offset) {
///   // Ensure the string is converted to UTF8 by using reflex::Input and
///   // instantiate a scanner.
///   reflex::Input input(input_string.data(), input_string.size());
///   static const string filename_str(filename);
///   return parser::Parse(input, &filename_str, line_offset);
/// }

// TODO(blais): Implement this.
/// std::unique_ptr<proto::Database> ParseStdin() {
///   reflex::Input input(stdin, reflex::Input::file_encoding::utf8);
///   static const string filename = "<string>";
///   auto db = parser::Parse(input, &filename);
///   fclose(stdin);
///   return db;
/// }

}  // namespace parser
}  // namespace beancount

//-----------------------------------------------------------------------------
// TODO in parser rewrite to C++:

// - Make it possible to tag/link before the narration strings.

// - Rework all the rules to take advantage of the name aliases; at least do it
// - uniformly.

// - In the metadata, output not just the starting line but also the end line,
//   for easy extraction of the original text.

// - Review all error logging in the scanner and parser.
//   The following can get logged; override the appropriate methods.
//   Grep in the implementations to ensure I've handled all of them.
//   /home/blais/r/q/office/accounting/blais.beancount:1591.2-0x7fff5298fdf8:1591.30: syntax error, unexpected ACCOUNT, expecting DEDENT or TAG or LINK or KEY
//     Assets:CA:LondonLife:RRSP:Cash   2000 CAD
//     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//
// - Add templated new/delete interfaces on the builder and count memory
//   allocations. I like to go raw in the parser (no smart pointers) but I
//   should automate checking, at least conditionally.
//
// - Move builder to its own compilation unit.
//
// - Expand the vocabulary of flags.
//   Unify HASH and ASTERISK flags, also with posting vs. transaction flags. Test explicitly in lexer.
//
// - Insert time for an additional sort-key.
//   Add datetime parsing (civil datetime is sufficient).
//
// - Add optional date parsing to postings.
//
// - Contemplate merging tags & links into a single field.
//
// - Review usage of string_view and temporary buffers. Try converting to
//   std::string and measure impact (may be minimal with judicious usage of
//   std::move()); {96464129e441}
